// Copyright © 2010 The CefSharp Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows.Forms;
using CefSharp.Example.JavascriptBinding;
using CefSharp.WinForms.Example.Handlers;
using MyCef.Example;
using MyCef.Handlers;

namespace CefSharp.WinForms.Example
{
    public partial class BrowserTabUserControl : UserControl
    {
        public IWinFormsWebBrowser Browser { get; private set; }
        private IntPtr browserHandle;
        private ChromeWidgetMessageInterceptor messageInterceptor;
        private bool multiThreadedMessageLoopEnabled;

        [return: MarshalAs(UnmanagedType.Bool)]
        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

        public BrowserTabUserControl(Action<string, int?> openNewTab, string url, bool multiThreadedMessageLoopEnabled)
        {
            this.Dock = DockStyle.Fill;
            InitializeComponent();
            var browser = new ChromiumWebBrowser(url)
            {
                Dock = DockStyle.Fill
            };
            browserPanel.Controls.Add(browser);
            Browser = browser;
            browser.MenuHandler = new MenuHandler();
            browser.RequestHandler = new WinFormsRequestHandler(openNewTab);
            //注册JS弹窗Alert这种
            browser.JsDialogHandler = new JsDialogHandler();
            browser.DownloadHandler = new DownloadHandler();
            // browser.AudioHandler = new AudioHandler();

            if (multiThreadedMessageLoopEnabled)
            {
                browser.KeyboardHandler = new KeyboardHandler();
            }
            else
            {
                //When MultiThreadedMessageLoop is disabled we don't need the CefSharp focus handler implementation.
                browser.FocusHandler = null;
            }


            browser.LifeSpanHandler = new LifeSpanHandler(openPopupsAsTabs: true);
            browser.LoadingStateChanged += OnBrowserLoadingStateChanged;
            browser.ConsoleMessage += OnBrowserConsoleMessage;
            browser.TitleChanged += OnBrowserTitleChanged;
            browser.AddressChanged += OnBrowserAddressChanged;
            browser.StatusMessage += OnBrowserStatusMessage;
            browser.IsBrowserInitializedChanged += OnIsBrowserInitializedChanged;
            browser.LoadError += OnLoadError;
            browser.JavascriptObjectRepository.Register("bound", new BoundObject(), isAsync: false, options: BindingOptions.DefaultBinder);
            browser.JavascriptObjectRepository.Register("boundAsync", new AsyncBoundObject(), isAsync: true, options: BindingOptions.DefaultBinder);

            //If you call CefSharp.BindObjectAsync in javascript and pass in the name of an object which is not yet
            //bound, then ResolveObject will be called, you can then register it
            browser.JavascriptObjectRepository.ResolveObject += (sender, e) =>
            {
                var repo = e.ObjectRepository;
                if (e.ObjectName == "boundAsync2")
                {
                    repo.Register("boundAsync2", new AsyncBoundObject(), isAsync: true, options: BindingOptions.DefaultBinder);
                }
            };

            browser.RenderProcessMessageHandler = new RenderProcessMessageHandler();
            browser.DisplayHandler = new DisplayHandler();
            //browser.MouseDown += OnBrowserMouseClick;
            browser.HandleCreated += OnBrowserHandleCreated;
            //browser.ResourceHandlerFactory = new FlashResourceHandlerFactory();
            this.multiThreadedMessageLoopEnabled = multiThreadedMessageLoopEnabled;

            var eventObject = new ScriptedMethodsBoundObject();
            eventObject.EventArrived += OnJavascriptEventArrived;
            // Use the default of camelCaseJavascriptNames
            // .Net methods starting with a capitol will be translated to starting with a lower case letter when called from js
            browser.JavascriptObjectRepository.Register("boundEvent", eventObject, isAsync: false, options: BindingOptions.DefaultBinder);

            CefExample.RegisterTestResources(browser);

            var version = string.Format("Chromium: {0}, CEF: {1}, CefSharp: {2}", Cef.ChromiumVersion, Cef.CefVersion, Cef.CefSharpVersion);
            //Set label directly, don't use DisplayOutput as call would be a NOOP (no valid handle yet).
            outputLabel.Text = version;
        }

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (components != null)
                {
                    components.Dispose();
                    components = null;
                }

                if (messageInterceptor != null)
                {
                    messageInterceptor.ReleaseHandle();
                    messageInterceptor = null;
                }
            }
            base.Dispose(disposing);
        }



        #region  事件
        private void OnBrowserHandleCreated(object sender, EventArgs e)
        {
            browserHandle = ((ChromiumWebBrowser)Browser).Handle;
        }

        /// <summary>
        /// 外部协议错误的时候，不要显示
        /// 在系统的 OnProtocolExecution里处理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnLoadError(object sender, LoadErrorEventArgs args)
        {
            if (args.ErrorCode == CefErrorCode.UnknownUrlScheme && args.Frame.Url.StartsWith("mailto"))
            {
                return;
            }

            DisplayOutput("Load Error:" + args.ErrorCode + ";" + args.ErrorText);
        }

        /// <summary>
        /// 浏览器控制台消息输出
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnBrowserConsoleMessage(object sender, ConsoleMessageEventArgs args)
        {
            DisplayOutput(string.Format("行 {0}, 代码Source: {1}, 消息: {2}", args.Line, args.Source, args.Message));
        }
        /// <summary>
        /// 浏览器状态栏消息
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnBrowserStatusMessage(object sender, StatusMessageEventArgs args)
        {
            this.InvokeOnUiThreadIfRequired(() => statusLabel.Text = args.Value);
        }
        /// <summary>
        /// 浏览器加载状态变化
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnBrowserLoadingStateChanged(object sender, LoadingStateChangedEventArgs args)
        {
            SetCanGoBack(args.CanGoBack);
            SetCanGoForward(args.CanGoForward);
            this.InvokeOnUiThreadIfRequired(() => SetIsLoading(args.IsLoading));
        }
        /// <summary>
        /// 标题发生变化
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnBrowserTitleChanged(object sender, TitleChangedEventArgs args)
        {
            this.InvokeOnUiThreadIfRequired(() => Parent.Text = args.Title);
        }
        /// <summary>
        /// 地址栏发生变化
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnBrowserAddressChanged(object sender, AddressChangedEventArgs args)
        {
            this.InvokeOnUiThreadIfRequired(() => urlTextBox.Text = args.Address);
        }
        /// <summary>
        /// 响应js事件
        /// </summary>
        /// <param name="eventName"></param>
        /// <param name="eventData"></param>
        private static void OnJavascriptEventArrived(string eventName, object eventData)
        {
            switch (eventName)
            {
                case "click":
                    {
                        var message = eventData.ToString();
                        var dataDictionary = eventData as Dictionary<string, object>;
                        if (dataDictionary != null)
                        {
                            var result = string.Join(", ", dataDictionary.Select(pair => pair.Key + "=" + pair.Value));
                            message = "数据是: " + result;
                        }
                        MessageBox.Show(message, "收到Javascript消息", MessageBoxButtons.OK, MessageBoxIcon.Information);
                        break;
                    }
            }
        }
        /// <summary>
        /// 初始化状变更
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnIsBrowserInitializedChanged(object sender, EventArgs e)
        {
            //获取底层浏览器主机
            var browserHost = Browser.GetBrowser().GetHost();
            var requestContext = browserHost.RequestContext;
            string errorMessage;
            //启用不跟踪模式
            var success = requestContext.SetPreference("enable_do_not_track", true, out errorMessage);
            if (!success)
            {
                this.InvokeOnUiThreadIfRequired(() => MessageBox.Show("无法启用“不跟踪错误”" + errorMessage));
            }

            //Example of disable spellchecking
            //success = requestContext.SetPreference("browser.enable_spellchecking", false, out errorMessage);

            var preferences = requestContext.GetAllPreferences(true);
            var doNotTrack = (bool)preferences["enable_do_not_track"];

            //Use this to check that settings preferences are working in your code
            //success = requestContext.SetPreference("webkit.webprefs.minimum_font_size", 24, out errorMessage);

            //If we're using CefSetting.MultiThreadedMessageLoop (the default) then to hook the message pump,
            // which running in a different thread we have to use a NativeWindow
            if (multiThreadedMessageLoopEnabled)
            {
                SetupMessageInterceptor();
            }
        }

        /// <summary>
        /// 工具栏加载事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HandleToolStripLayout(object sender, LayoutEventArgs e)
        {
            HandleToolStripLayout();
        }
        private void GoButtonClick(object sender, EventArgs e)
        {
            LoadUrl(urlTextBox.Text);
        }

        private void BackButtonClick(object sender, EventArgs e)
        {
            Browser.Back();
        }

        private void ForwardButtonClick(object sender, EventArgs e)
        {
            Browser.Forward();
        }
        private void UrlTextBoxKeyUp(object sender, KeyEventArgs e)
        {
            if (e.KeyCode != Keys.Enter)
            {
                return;
            }

            LoadUrl(urlTextBox.Text);
        }
        private void FindNextButtonClick(object sender, EventArgs e)
        {
            Find(true);
        }

        private void FindPreviousButtonClick(object sender, EventArgs e)
        {
            Find(false);
        }

        private void FindCloseButtonClick(object sender, EventArgs e)
        {
            ToggleBottomToolStrip();
        }
        private void FindTextBoxKeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode != Keys.Enter)
            {
                return;
            }
            Find(true);
        }
        #endregion
        #region  方法
        /// <summary>
        /// 是否可回退
        /// </summary>
        /// <param name="canGoBack"></param>
        private void SetCanGoBack(bool canGoBack)
        {
            this.InvokeOnUiThreadIfRequired(() => backButton.Enabled = canGoBack);
        }
        /// <summary>
        /// 是否可以前进
        /// </summary>
        /// <param name="canGoForward"></param>
        private void SetCanGoForward(bool canGoForward)
        {
            this.InvokeOnUiThreadIfRequired(() => forwardButton.Enabled = canGoForward);
        }
        /// <summary>
        /// 是否正在加载
        /// </summary>
        /// <param name="isLoading"></param>
        private void SetIsLoading(bool isLoading)
        {
            goButton.Text = isLoading ? "停止" : "转到";
            goButton.Image = isLoading ? global::MyCef.Resource1.nav_plain_red : global::MyCef.Resource1.nav_left_green;
            HandleToolStripLayout();
        }

        /// <summary>
        /// 输出方法
        /// </summary>
        /// <param name="output"></param>
        private void DisplayOutput(string output)
        {
            outputLabel.InvokeOnUiThreadIfRequired(() => outputLabel.Text = output);
        }
        /// <summary>
        /// 本控件不触发MouseEnter/Move/Leave事件，因为Chromium本身处理了这些事件。
        /// 此方法提供了一个钩住Chrome_RenderWidgetHostHWND句柄以接收低级消息的演示。
        /// 您可以使用这种技术，拖/拽等方法来钩住其他窗口消息
        /// </summary>
        private void SetupMessageInterceptor()
        {
            if (messageInterceptor != null)
            {
                messageInterceptor.ReleaseHandle();
                messageInterceptor = null;
            }

            Task.Run(async () =>
            {
                try
                {
                    while (true)
                    {
                        IntPtr chromeWidgetHostHandle;
                        if (ChromeWidgetHandleFinder.TryFindHandle(browserHandle, out chromeWidgetHostHandle))
                        {
                            messageInterceptor = new ChromeWidgetMessageInterceptor((Control)Browser, chromeWidgetHostHandle, message =>
                            {
                                const int WM_MOUSEACTIVATE = 0x0021;
                                const int WM_NCLBUTTONDOWN = 0x00A1;
                                const int WM_DESTROY = 0x0002;

                                // Render process switch happened, need to find the new handle
                                if (message.Msg == WM_DESTROY)
                                {
                                    SetupMessageInterceptor();
                                    return;
                                }

                                if (message.Msg == WM_MOUSEACTIVATE)
                                {
                                    // The default processing of WM_MOUSEACTIVATE results in MA_NOACTIVATE,
                                    // and the subsequent mouse click is eaten by Chrome.
                                    // This means any .NET ToolStrip or ContextMenuStrip does not get closed.
                                    // By posting a WM_NCLBUTTONDOWN message to a harmless co-ordinate of the
                                    // top-level window, we rely on the ToolStripManager's message handling
                                    // to close any open dropdowns:
                                    // http://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/ToolStripManager.cs,1249
                                    var topLevelWindowHandle = message.WParam;
                                    PostMessage(topLevelWindowHandle, WM_NCLBUTTONDOWN, IntPtr.Zero, IntPtr.Zero);
                                }
                                //Forward mouse button down message to browser control
                                //else if(message.Msg == WM_LBUTTONDOWN)
                                //{
                                //    PostMessage(browserHandle, WM_LBUTTONDOWN, message.WParam, message.LParam);
                                //}

                                // The ChromiumWebBrowserControl does not fire MouseEnter/Move/Leave events, because Chromium handles these.
                                // However we can hook into Chromium's messaging window to receive the events.
                                //
                                //const int WM_MOUSEMOVE = 0x0200;
                                //const int WM_MOUSELEAVE = 0x02A3;
                                //
                                //switch (message.Msg) {
                                //    case WM_MOUSEMOVE:
                                //        Console.WriteLine("WM_MOUSEMOVE");
                                //        break;
                                //    case WM_MOUSELEAVE:
                                //        Console.WriteLine("WM_MOUSELEAVE");
                                //        break;
                                //}
                            });

                            break;
                        }
                        else
                        {
                            // Chrome hasn't yet set up its message-loop window.
                            await Task.Delay(10);
                        }
                    }
                }
                catch
                {
                    // Errors are likely to occur if browser is disposed, and no good way to check from another thread
                }
            });
        }
        /// <summary>
        /// 工具栏加载
        /// </summary>
        private void HandleToolStripLayout()
        {
            var width = toolStrip1.Width;
            foreach (ToolStripItem item in toolStrip1.Items)
            {
                if (item != urlTextBox)
                {
                    width -= item.Width - item.Margin.Horizontal;
                }
            }
            urlTextBox.Width = Math.Max(0, width - urlTextBox.Margin.Horizontal - 18);
        }
        /// <summary>
        /// 打开URL
        /// </summary>
        /// <param name="url"></param>
        private void LoadUrl(string url)
        {
            if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute))
            {
                Browser.Load(url);
            }
            else
            {
                var searchUrl = "https://www.baidu.com/s?ie=UTF-8&wd=" + Uri.EscapeDataString(url);
                Browser.Load(searchUrl);
            }
        }
        /// <summary>
        /// 拷贝到剪切板
        /// </summary>
        public async void CopySourceToClipBoardAsync()
        {
            var htmlSource = await Browser.GetSourceAsync();
            Clipboard.SetText(htmlSource);
            DisplayOutput("已经复制HTML到剪切板");
        }

        private void ToggleBottomToolStrip()
        {
            if (toolStrip2.Visible)
            {
                Browser.StopFinding(true);
                toolStrip2.Visible = false;
            }
            else
            {
                toolStrip2.Visible = true;
                findTextBox.Focus();
            }
        }
        /// <summary>
        /// 查找
        /// </summary>
        /// <param name="next"></param>
        private void Find(bool next)
        {
            if (!string.IsNullOrEmpty(findTextBox.Text))
            {
                Browser.Find(0, findTextBox.Text, next, false, false);
            }
        }
        /// <summary>
        /// 停靠在现有UserControl中的DevTools示例，在本例中，它被托管在一个带有SplitContainer的面板中
        /// </summary>

        public void ShowDevToolsDocked()
        {
            if (browserSplitContainer.Panel2Collapsed)
            {
                browserSplitContainer.Panel2Collapsed = false;
            }

            //Find devToolsControl in Controls collection
            DevToolsContainerControl devToolsControl = null;
            devToolsControl = browserSplitContainer.Panel2.Controls.Find(nameof(devToolsControl), false).FirstOrDefault() as DevToolsContainerControl;

            if (devToolsControl == null || devToolsControl.IsDisposed)
            {
                devToolsControl = new DevToolsContainerControl()
                {
                    Name = nameof(devToolsControl),
                    Dock = DockStyle.Fill
                };

                EventHandler devToolsPanelDisposedHandler = null;
                devToolsPanelDisposedHandler = (s, e) =>
                {
                    browserSplitContainer.Panel2.Controls.Remove(devToolsControl);
                    browserSplitContainer.Panel2Collapsed = true;
                    devToolsControl.Disposed -= devToolsPanelDisposedHandler;
                };

                //Subscribe for devToolsPanel dispose event
                devToolsControl.Disposed += devToolsPanelDisposedHandler;

                //Add new devToolsPanel instance to Controls collection
                browserSplitContainer.Panel2.Controls.Add(devToolsControl);
            }

            if (!devToolsControl.IsHandleCreated)
            {
                //It's very important the handle for the control is created prior to calling
                //SetAsChild, if the handle hasn't been created then manually call CreateControl();
                //This code is not required for this example, it's left here for demo purposes.
                devToolsControl.CreateControl();
            }

            //Devtools will be a child of the DevToolsContainerControl
            //DevToolsContainerControl is a simple custom Control that's only required
            //when CefSettings.MultiThreadedMessageLoop = false so arrow/tab key presses
            //are forwarded to DevTools correctly.
            var rect = devToolsControl.ClientRectangle;
            var windowInfo = new WindowInfo();
            windowInfo.SetAsChild(devToolsControl.Handle, rect.Left, rect.Top, rect.Right, rect.Bottom);
            Browser.GetBrowserHost().ShowDevTools(windowInfo);
        }

        public void ShowFind()
        {
            ToggleBottomToolStrip();
        }


        public Task<bool> CheckIfDevToolsIsOpenAsync()
        {
            return Cef.UIThreadTaskFactory.StartNew(() =>
            {
                return Browser.GetBrowserHost().HasDevTools;
            });
        }

        public void changeToolScripVisable(bool visable)
        {
            this.toolStrip1.Visible = visable;
        }
    }
    #endregion
}
